﻿using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Linq;
using UnityEngine.Events;

namespace Com.LuisPedroFonseca.ProCamera2D
{
    [Serializable]
    public class CinematicTarget
    {
        public Transform TargetTransform;
        public float EaseInDuration = 1f;
        public float HoldDuration = 1f;
        public float Zoom = 1f;
        public EaseType EaseType = EaseType.EaseOut;
        public string SendMessageName;
        public string SendMessageParam;
    }

    [Serializable]
    public class CinematicEvent : UnityEvent<int>
    {
    }

    #if UNITY_5_3_OR_NEWER
    [HelpURL("http://www.procamera2d.com/user-guide/extension-cinematics/")]
    #endif
    public class ProCamera2DCinematics : BasePC2D, IPositionOverrider, ISizeOverrider
    {
        public static string ExtensionName = "Cinematics";

        public UnityEvent OnCinematicStarted;
        public CinematicEvent OnCinematicTargetReached;
        public UnityEvent OnCinematicFinished;

        bool _isPlaying;

        public bool IsPlaying { get { return _isPlaying; } }

        public List<CinematicTarget> CinematicTargets = new List<CinematicTarget>();
        public float EndDuration = 1f;
        public EaseType EndEaseType = EaseType.EaseOut;

        public bool UseNumericBoundaries;

        public bool UseLetterbox = true;

        [Range(0f, .5f)]
        public float LetterboxAmount = .1f;

        public float LetterboxAnimDuration = 1f;

        public Color LetterboxColor = Color.black;

        float _initialCameraSize;

        ProCamera2DNumericBoundaries _numericBoundaries;

        ProCamera2DLetterbox _letterbox;

        Coroutine _startCinematicRoutine;
        Coroutine _goToCinematicRoutine;
        Coroutine _endCinematicRoutine;

        bool _skipTarget;

        Vector3 _newPos;
        Vector3 _originalPos;
        Vector3 _startPos;

        float _newSize;
        bool _paused;

        override protected void Awake()
        {
            base.Awake();

            if (UseLetterbox)
                SetupLetterbox();
            
            ProCamera2D.AddPositionOverrider(this);
            ProCamera2D.AddSizeOverrider(this);
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();

            ProCamera2D.RemovePositionOverrider(this);
            ProCamera2D.RemoveSizeOverrider(this);
        }

        #region IPositionOverrider implementation

        public Vector3 OverridePosition(float deltaTime, Vector3 originalPosition)
        {
            if (!enabled)
                return originalPosition;
            
            _originalPos = originalPosition;

            if (_isPlaying)
                return _newPos;
            else
                return originalPosition;
        }

        public int POOrder { get { return _poOrder; } set { _poOrder = value; } }

        int _poOrder = 0;

        #endregion

        #region ISizeOverrider implementation

        public float OverrideSize(float deltaTime, float originalSize)
        {
            if (!enabled)
                return originalSize;
            
            if (_isPlaying)
                return _newSize;
            else
                return originalSize;
        }

        public int SOOrder { get { return _soOrder; } set { _soOrder = value; } }

        int _soOrder = 3000;

        #endregion

        /// <summary>Play the cinematic.</summary>
        public void Play()
        {
            if (_isPlaying)
                return;

            _paused = false;

            if (CinematicTargets.Count == 0)
            {
                Debug.LogWarning("No cinematic targets added to the list");
                return;
            }

            _initialCameraSize = ProCamera2D.ScreenSizeInWorldCoordinates.y * .5f;

            if (UseNumericBoundaries && _numericBoundaries == null)
                _numericBoundaries = ProCamera2D.GetComponentInChildren<ProCamera2DNumericBoundaries>();

            if (_numericBoundaries == null)
                UseNumericBoundaries = false;

            _isPlaying = true;
            if (_endCinematicRoutine != null)
            {
                StopCoroutine(_endCinematicRoutine);
                _endCinematicRoutine = null;
            }

            if (_startCinematicRoutine == null)
                _startCinematicRoutine = StartCoroutine(StartCinematicRoutine());
        }

        /// <summary>Stop the cinematic.</summary>
        public void Stop()
        {
            if (!_isPlaying)
                return;

            if (_startCinematicRoutine != null)
            {
                StopCoroutine(_startCinematicRoutine);
                _startCinematicRoutine = null;
            }

            if (_goToCinematicRoutine != null)
            {
                StopCoroutine(_goToCinematicRoutine);
                _goToCinematicRoutine = null;
            }

            if (_endCinematicRoutine == null)
                _endCinematicRoutine = StartCoroutine(EndCinematicRoutine());
        }

        /// <summary>If the cinematic is stopped, it plays it. If it's playing, it stops it.</summary>
        public void Toggle()
        {
            if (_isPlaying)
                Stop();
            else
                Play();
        }

        /// <summary>Goes to the next cinematic target</summary>
        public void GoToNextTarget()
        {
            _skipTarget = true;
        }

        /// <summary>Pauses the cinematic</summary>
        public void Pause()
        {
            _paused = true;
        }

        /// <summary>Unpauses the cinematic</summary>
        public void Unpause()
        {
            _paused = false;
        }

        /// <summary>Goes to the next cinematic target</summary>
        /// <param name="targetTransform">The Transform component of the target</param>
        /// <param name="easeInDuration">The time it takes for the camera to reach the target</param>
        /// <param name="holdDuration">The time the camera follows the target. If below 0, you’ll have to manually move to the next target by using the GoToNextTarget method</param>
        /// <param name="zoom">The zoom the camera should make while following the target. Use 1 for no zoom</param>
        /// <param name="easeType">The animation type of the camera movement to reach the target</param>
        /// <param name="sendMessageName">The method name that will be called when the target is reached</param>
        /// <param name="sendMessageParam">The parameter that will be sent when the above method is called</param>
        /// <param name="index">The position in the targets list. Use -1 to put in last and 0 for first</param>
        public void AddCinematicTarget(Transform targetTransform, float easeInDuration = 1f, float holdDuration = 1f, float zoom = 1f, EaseType easeType = EaseType.EaseOut, string sendMessageName = "", string sendMessageParam = "", int index = -1)
        {
            var newCinematicTarget = new CinematicTarget()
            {
                TargetTransform = targetTransform,
                EaseInDuration = easeInDuration,
                HoldDuration = holdDuration,
                Zoom = zoom,
                EaseType = easeType,
                SendMessageName = sendMessageName,
                SendMessageParam = sendMessageParam
            };
            
            if (index == -1 || index > CinematicTargets.Count)
                CinematicTargets.Add(newCinematicTarget);
            else
                CinematicTargets.Insert(index, newCinematicTarget);
        }

        // <summary>Remove a cinematic target</summary>
        /// <param name="targetTransform">The Transform component of the target</param>
        public void RemoveCinematicTarget(Transform targetTransform)
        {
            for (int i = 0; i < CinematicTargets.Count; i++)
            {
                if (CinematicTargets[i].TargetTransform.GetInstanceID() == targetTransform.GetInstanceID())
                {
                    CinematicTargets.Remove(CinematicTargets[i]);
                }
            }
        }

        IEnumerator StartCinematicRoutine()
        {
            if (OnCinematicStarted != null)
                OnCinematicStarted.Invoke();

            _startPos = ProCamera2D.LocalPosition;
            _newPos = ProCamera2D.LocalPosition;
            _newSize = ProCamera2D.ScreenSizeInWorldCoordinates.y * .5f;

            if (UseLetterbox)
            {
                if (_letterbox == null)
                    SetupLetterbox();

                _letterbox.Color = LetterboxColor;
                _letterbox.TweenTo(LetterboxAmount, LetterboxAnimDuration);
            }

            var count = -1;
            while (count < CinematicTargets.Count - 1)
            {
                count++;
                _skipTarget = false;
                _goToCinematicRoutine = StartCoroutine(GoToCinematicTargetRoutine(CinematicTargets[count], count));
                yield return _goToCinematicRoutine;
            }

            Stop();
        }

        IEnumerator GoToCinematicTargetRoutine(CinematicTarget cinematicTarget, int targetIndex)
        {
            if (cinematicTarget.TargetTransform == null)
                yield break;

            var initialPosH = Vector3H(ProCamera2D.LocalPosition);
            var initialPosV = Vector3V(ProCamera2D.LocalPosition);

            var currentCameraSize = ProCamera2D.ScreenSizeInWorldCoordinates.y * .5f;

            // Ease in
            var t = 0f;
            if (cinematicTarget.EaseInDuration > 0)
            {
                while (t <= 1.0f)
                {
                    if (!_paused)
                    {
                        t += ProCamera2D.DeltaTime / cinematicTarget.EaseInDuration;

                        var newPosH = Utils.EaseFromTo(initialPosH, Vector3H(cinematicTarget.TargetTransform.position) - Vector3H(ProCamera2D.ParentPosition), t, cinematicTarget.EaseType);
                        var newPosV = Utils.EaseFromTo(initialPosV, Vector3V(cinematicTarget.TargetTransform.position) - Vector3V(ProCamera2D.ParentPosition), t, cinematicTarget.EaseType);

                        if (UseNumericBoundaries)
                            LimitToNumericBoundaries(ref newPosH, ref newPosV);

                        _newPos = VectorHVD(newPosH, newPosV, 0);

                        _newSize = Utils.EaseFromTo(currentCameraSize, _initialCameraSize / cinematicTarget.Zoom, t, cinematicTarget.EaseType);

                        if (_skipTarget)
                            yield break;
                    }

                    yield return ProCamera2D.GetYield();
                }
            }
            else
            {
                var newPosH = Vector3H(cinematicTarget.TargetTransform.position) - Vector3H(ProCamera2D.ParentPosition);
                var newPosV = Vector3V(cinematicTarget.TargetTransform.position) - Vector3V(ProCamera2D.ParentPosition);
                _newPos = VectorHVD(newPosH, newPosV, 0);

                _newSize = _initialCameraSize / cinematicTarget.Zoom;
            }

            // Dispatch target reached event
            if (OnCinematicTargetReached != null)
                OnCinematicTargetReached.Invoke(targetIndex);

            // Send target reached message
            if (!string.IsNullOrEmpty(cinematicTarget.SendMessageName))
            {
                cinematicTarget.TargetTransform.SendMessage(cinematicTarget.SendMessageName, cinematicTarget.SendMessageParam, SendMessageOptions.DontRequireReceiver);
            }

            // Hold
            t = 0f;
            while (cinematicTarget.HoldDuration < 0 || t <= cinematicTarget.HoldDuration)
            {
                if (!_paused)
                {
                    t += ProCamera2D.DeltaTime;

                    var newPosH = Vector3H(cinematicTarget.TargetTransform.position) - Vector3H(ProCamera2D.ParentPosition);
                    var newPosV = Vector3V(cinematicTarget.TargetTransform.position) - Vector3V(ProCamera2D.ParentPosition);

                    if (UseNumericBoundaries)
                        LimitToNumericBoundaries(ref newPosH, ref newPosV);

                    _newPos = VectorHVD(newPosH, newPosV, 0);

                    if (_skipTarget)
                        yield break;
                }

                yield return ProCamera2D.GetYield();
            }
        }

        IEnumerator EndCinematicRoutine()
        {
            if (_letterbox != null && LetterboxAmount > 0)
                _letterbox.TweenTo(0f, LetterboxAnimDuration);
            
            var initialPosH = Vector3H(_newPos);
            var initialPosV = Vector3V(_newPos);

            var currentCameraSize = ProCamera2D.ScreenSizeInWorldCoordinates.y * .5f;

            // Ease out
            var t = 0f;
            while (t <= 1.0f)
            {
                if (!_paused)
                {
                    t += ProCamera2D.DeltaTime / EndDuration;

                    float newPosH = 0f;
                    float newPosV = 0f;
                    if (ProCamera2D.CameraTargets.Count > 0)
                    {
                        newPosH = Utils.EaseFromTo(initialPosH, Vector3H(_originalPos), t, EndEaseType);
                        newPosV = Utils.EaseFromTo(initialPosV, Vector3V(_originalPos), t, EndEaseType);
                    }
                    else
                    {
                        newPosH = Utils.EaseFromTo(initialPosH, Vector3H(_startPos), t, EndEaseType);
                        newPosV = Utils.EaseFromTo(initialPosV, Vector3V(_startPos), t, EndEaseType);
                    }

                    if (UseNumericBoundaries)
                        LimitToNumericBoundaries(ref newPosH, ref newPosV);

                    _newPos = VectorHVD(newPosH, newPosV, 0);

                    _newSize = Utils.EaseFromTo(currentCameraSize, _initialCameraSize, t, EndEaseType);
                }

                yield return ProCamera2D.GetYield();
            }

            if (OnCinematicFinished != null)
                OnCinematicFinished.Invoke();

            _isPlaying = false;

            // Ugly hack... but no way around it at the moment
            if (ProCamera2D.CameraTargets.Count == 0)
                ProCamera2D.Reset(true);
        }

        void SetupLetterbox()
        {
            var letterbox = ProCamera2D.gameObject.GetComponentInChildren<ProCamera2DLetterbox>();

            if (letterbox == null)
            {
                var cameras = ProCamera2D.gameObject.GetComponentsInChildren<Camera>();
                cameras = cameras.OrderByDescending(c => c.depth).ToArray();
                cameras[0].gameObject.AddComponent<ProCamera2DLetterbox>();
            }

            _letterbox = letterbox;
        }

        void LimitToNumericBoundaries(ref float horizontalPos, ref float verticalPos)
        {
            if (_numericBoundaries.UseLeftBoundary && horizontalPos - ProCamera2D.ScreenSizeInWorldCoordinates.x / 2 < _numericBoundaries.LeftBoundary)
                horizontalPos = _numericBoundaries.LeftBoundary + ProCamera2D.ScreenSizeInWorldCoordinates.x / 2;
            else if (_numericBoundaries.UseRightBoundary && horizontalPos + ProCamera2D.ScreenSizeInWorldCoordinates.x / 2 > _numericBoundaries.RightBoundary)
                horizontalPos = _numericBoundaries.RightBoundary - ProCamera2D.ScreenSizeInWorldCoordinates.x / 2;

            if (_numericBoundaries.UseBottomBoundary && verticalPos - ProCamera2D.ScreenSizeInWorldCoordinates.y / 2 < _numericBoundaries.BottomBoundary)
                verticalPos = _numericBoundaries.BottomBoundary + ProCamera2D.ScreenSizeInWorldCoordinates.y / 2;
            else if (_numericBoundaries.UseTopBoundary && verticalPos + ProCamera2D.ScreenSizeInWorldCoordinates.y / 2 > _numericBoundaries.TopBoundary)
                verticalPos = _numericBoundaries.TopBoundary - ProCamera2D.ScreenSizeInWorldCoordinates.y / 2;
        }

        #if UNITY_EDITOR
        override protected void DrawGizmos()
        {
            base.DrawGizmos();

            float cameraDepthOffset = Vector3D(ProCamera2D.transform.localPosition) + Mathf.Abs(Vector3D(ProCamera2D.transform.localPosition)) * Vector3D(ProCamera2D.transform.forward);

            // Draw cinematic targets
            for (int i = 0; i < CinematicTargets.Count; i++)
            {
                if (CinematicTargets[i].TargetTransform != null)
                {
                    var targetPos = VectorHVD(Vector3H(CinematicTargets[i].TargetTransform.position), Vector3V(CinematicTargets[i].TargetTransform.position), cameraDepthOffset);
                    Gizmos.DrawIcon(targetPos, "ProCamera2D/gizmo_icon_exclusive_free.png", false);
                }
            }
        }

        void OnDrawGizmosSelected()
        {
            if (ProCamera2D == null)
                return;
            
            var gameCamera = ProCamera2D.GetComponent<Camera>();
            float cameraDepthOffset = Vector3D(ProCamera2D.transform.localPosition) + Mathf.Abs(Vector3D(ProCamera2D.transform.localPosition)) * Vector3D(ProCamera2D.transform.forward);
            var cameraDimensions = Utils.GetScreenSizeInWorldCoords(gameCamera, Mathf.Abs(Vector3D(ProCamera2D.transform.localPosition)));

            // Draw cinematic path
            GUIStyle style = new GUIStyle();
            style.normal.textColor = Color.green;
            for (int i = 0; i < CinematicTargets.Count; i++)
            {
                var targetPos = VectorHVD(Vector3H(CinematicTargets[i].TargetTransform.position), Vector3V(CinematicTargets[i].TargetTransform.position), cameraDepthOffset);

                if (i > 0)
                {
                    UnityEditor.Handles.DrawLine(targetPos, VectorHVD(Vector3H(CinematicTargets[i - 1].TargetTransform.position), Vector3V(CinematicTargets[i - 1].TargetTransform.position), cameraDepthOffset));
                }

                UnityEditor.Handles.color = Color.blue;
                if (i < CinematicTargets.Count - 1)
                {
                    var nextTargetPos = VectorHVD(Vector3H(CinematicTargets[i + 1].TargetTransform.position), Vector3V(CinematicTargets[i + 1].TargetTransform.position), cameraDepthOffset);
                    var arrowSize = cameraDimensions.x * .1f;
                    if ((nextTargetPos - targetPos).magnitude > arrowSize)
                    {
#if UNITY_5_5_OR_NEWER
                        UnityEditor.Handles.ArrowHandleCap(
                            i, 
                            targetPos, 
                            Quaternion.LookRotation(nextTargetPos - targetPos), 
                            cameraDimensions.x * .1f,
                            EventType.Repaint);
#else
                        UnityEditor.Handles.ArrowCap(
                            i, 
                            targetPos, 
                            Quaternion.LookRotation(nextTargetPos - targetPos), 
                            cameraDimensions.x * .1f);
#endif
                    }
                }
            }
        }
        #endif
    }
}